function LunarDate() {
    this.day = 0;
    this.month = 0;
    this.year = 0;
    this.isLeapMonth = false;
    this.leapMonth = NaN;
    
    this.toString = function() {
        var text = "";
        text += this.day + "/";
        text += this.month + (this.isLeapMonth ? "(*)" : "")  + "/";
        text += this.year;
        if(this.leapMonth && !this.isLeapMonth) {
            text += " (" + this.leapMonth + "*)";
        }
        return text;
    }
}
function VieCalendar(date) {
    const MINOR_TERMS = ["Giáp", "Ất", "Bính", "Đinh", "Mậu", "Kỷ", "Canh", "Tân", "Nhâm", "Quý"];
    const MAJOR_TERMS = ["Tý", "Sửu", "Dần", "Mão", "Thìn", "Tỵ", "Ngọ", "Mùi", "Thân", "Dậu", "Tuất", "Hợi"];
    const VI_WEEKS = ["Chủ Nhật", "Thứ Hai", "Thứ Ba", "Thứ Tư", "Thứ Năm", "Thứ Sáu", "Thứ Bảy"];
    const EN_WEEKS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
    const VI_MONTHS = ["Tháng Giêng", "Tháng Hai", "Tháng Ba", "Tháng Tư", "Tháng Năm", "Tháng Sáu",
                     "Tháng Bảy", "Tháng Tám", "Tháng Chín", "Tháng Mười", "Tháng M.Một", "Tháng Chạp"];
    const EN_MONTHS = ["January", "February", "March", "April", "May", "June",
                     "July", "August", "September", "October", "November", "December"];
    const ZODIAC_HOURS = ["110100101100", "001101001011", "110011010010",
                        "101100110100", "001011001101", "010010110011"];
    const SOLAR_TERMS = ["Xuân Phân", "Thanh Minh", "Cốc Vũ", "Lập Hạ",
                       "Tiểu Mãn", "Mang Chủng", "Hạ Chí", "Tiểu Thử", "Đại Thử",
                       "Lập Thu", "Xử Thử", "Bạch Lộ", "Thu Phân", "Hàn Lộ",
                       "Sương Giáng", "Lập Đông", "Tiểu Tuyết", "Đại Tuyết", "Đông Chí",
                       "Tiểu Hàn", "Đại Hàn", "Lập Xuân", "Vũ Thủy", "Kinh Trập"];
    const DAY = "ngày";
    const MONTH = "tháng";
    const YEAR = "năm";
    const LEAP = "(nhuận)";
    const LEAP2 = "(nhuận tháng x)";
    const TIMEZONE = 7;
    
    var day, month, year;
    var lunarDate;
    var juliusDays;
    
    this.setSolarDate = function(date) {
        if(!date || !date.getDate()) {
            date = new Date();
        }
        day = date.getDate();
        month = date.getMonth() + 1;
        year = date.getFullYear();
        juliusDays = calculateJuliusDays(day, month, year);
        lunarDate = caculateLunarDate(juliusDays, year);
        
        return this;
    }
    
    this.setSolarDate(date);    

    this.getWeekDay = function() {
        // 0: Sunday, 1: Monday, ..., 7: Saturday
        return (juliusDays + 1) % 7;
    };

    this.getWeekDayName = function(language) {
        var weekDay = this.getWeekDay();
        switch(language) {
            case "en":
                return EN_WEEKS[weekDay];
            default:
                return VI_WEEKS[weekDay];
        }
        
    };

    this.getSolarDay = function() {
        return day;
    };

    this.getSolarMonth = function() {
        return month;
    };

    this.getSolarMonthName = function(language) {
        switch(language) {
            case "en":
                return EN_MONTHS[month - 1];
            default:
                return VI_MONTHS[month - 1];
        }
    };

    this.getSolarYear = function() {
        return year;
    };

    this.getSolarTerm = function() {
        return SOLAR_TERMS[calculateSunLongitude(juliusDays + 1, TIMEZONE)];
    };

    this.isSolarLeapYear = function() {
        return (year % 4 == 0) || ((year % 100 == 0) && (year % 400 != 0));
    };

    this.isSolarClearMonth = function() {
        return (day == 1) || (day == 3) || (day == 5) || (day == 7)
                || (day == 8) || (day == 10) || (day == 12);
    };
    
    this.getJuliusDays = function() {
        return juliusDays;
    }

    this.getLunarDay = function() {
        return lunarDate.day;
    };

    this.getLunarDayName = function() {
        var name = "";
        name += MINOR_TERMS[(juliusDays + 9) % 10];
        name += " ";
        name += MAJOR_TERMS[(juliusDays + 1) % 12];
        
        return name;
    };

    this.getLunarMonth = function() {
        return lunarDate.month;
    };

    this.getLunarMonthName = function(language) {
        if(language) {
            switch(language) {
                case "en":
                    return EN_MONTHS[lunarDate.month - 1];
                default:
                    return VI_MONTHS[lunarDate.month - 1];
            }     
        }
        
        var name = "";
        name += MINOR_TERMS[(lunarDate.year * 12 + lunarDate.month + 3) % 10];
        name += " ";
        name += MAJOR_TERMS[(lunarDate.month + 1) % 12];
        if (lunarDate.leap) {
            name += " " + LEAP;
        }
        
        return name;
    };

    this.getLunarYear = function() {
        return lunarDate.year;
    };

    this.getLunarYearName = function() {
        var name = "";
        name += MINOR_TERMS[(lunarDate.year + 6) % 10];
        name += " ";
        name += MAJOR_TERMS[(lunarDate.year + 8) % 12];
        
        return name;
    };

    this.getLunarStartHourName = function() {
        var name = "";
        name += MINOR_TERMS[(juliusDays - 1)*2 % 10];
        name += " ";
        name += MAJOR_TERMS[0];
        
        return name;
    };

    this.getZodiacHourNames = function() {
        var chiOfDay = (juliusDays + 1) % 12;
        var zodiacHour = ZODIAC_HOURS[chiOfDay % 6];
    
        var hours = new Array();
        var count = 0;
        for (var i = 0; i < 12; i++) {
            if (zodiacHour.charAt(i) == '1') {
                hours.push(MAJOR_TERMS[i]);
            }
        }
        return hours;
    };

    this.getLunarLeapMonth = function() {
        return lunarDate.leapMonth;
    };

    this.isLunarLeapYear = function() {
        return !isNaN(lunarDate.leapMonth);
    };

    this.isLunarLeapMonth = function() {
        return lunarDate.isLeapMonth;
    };

    this.isLunarClearMonth = function() {
        if(lunarDate.day == 30) {
            return true;            
        }
        var remaiderDays = 30 - lunarDate.day;
        var juliusDaysForTest = juliusDays + remaiderDays;
        var lunarDateForTest = caculateLunarDate(juliusDaysForTest, year);
        
        return (lunarDateForTest.month == lunarDate.month);
    };
    
    this.toSolarDateString = function() {
        var text = "";
        text += this.getWeekDayName() + ", ";
        text += DAY + " " + this.getSolarDay() + " ";
        text += this.getSolarMonthName() + " ";
        text += YEAR + " " + this.getSolarYear();
        
        return text;
    }
    
    this.toLunarDateString = function() {
        var text = "";
        text += DAY + " " + this.getLunarDayName() + " ";
        text += MONTH + " " + this.getLunarMonthName() + " ";
        if(this.isLunarLeapMonth()) {
            text += LEAP + " ";
        }
        text += YEAR + " " + this.getLunarYearName();
        if(this.isLunarLeapYear() && !this.isLunarLeapMonth()) {
            var leap = LEAP2;
            text += " " + leap.replace("x", this.getLunarLeapMonth()); 
        }
        
        return text;
    }
    
    this.toDateString = function() {
        var text = "";
        text += this.toSolarDateString() + "\n";
        text += this.toLunarDateString();
        
        return text;
    }

    function caculateLunarDate(juliusDays, year) {
        var lunarDate = new LunarDate();
        var k = Math.floor((juliusDays - 2415021.076998695) / 29.530588853);
        var monthStart = calculateNewMoonDay(k+1, TIMEZONE);
        if (monthStart > juliusDays) {
            monthStart = calculateNewMoonDay(k, TIMEZONE);
        }
                
        var a11 = calculateLunarMonth11(year, TIMEZONE);
        var b11 = a11;
        if (a11 >= monthStart) {
            lunarDate.year = year;
            a11 = calculateLunarMonth11(year - 1, TIMEZONE);
        } else {
            lunarDate.year = year+1;
            b11 = calculateLunarMonth11(year + 1, TIMEZONE);
        }
                
        lunarDate.day = juliusDays - monthStart + 1;
        var diff = Math.floor((monthStart - a11)/29);
        lunarDate.leap = false;
        lunarDate.month = diff + 11;
        if (b11 - a11 > 365) {
            var leapMonthDiff = calculateLeapMonthOffset(a11, TIMEZONE);
            if (diff >= leapMonthDiff) {
                lunarDate.leapMonth = (leapMonthDiff - 2);
                lunarDate.month = diff + 10;
                if (diff == leapMonthDiff) {
                    lunarDate.isLeapMonth = true;
                }
            }
        }
        if (lunarDate.month > 12) {
            lunarDate.month = lunarDate.month - 12;
        }
        if (lunarDate.month >= 11 && diff < 4) {
            lunarDate.year -= 1;
        }
        
        return lunarDate;
    }

    function calculateJuliusDays(day, month, year) {
        var a = Math.floor((14 - month) / 12);
        var years = year + 4800 - a;
        var months  = month + 12*a - 3;
        var juliusDays = day + Math.floor((153*months + 2) / 5) + 365*years + Math.floor(years/4) - Math.floor(years/100) + Math.floor(years/400) - 32045;
        if (juliusDays < 2299161) {
            juliusDays = day + Math.floor((153*months + 2) / 5) + 365*years + Math.floor(years/4) - 32083;
        }
        return juliusDays;
    }
    
    function calculateNewMoonDay(k, timeZone) {
        var time1 = k / 1236.85; // Time in Julian centuries from 1900 January 0.5
        var time2 = time1 * time1;
        var time3 = time2 * time1;
        var time4 = time3 * time1;
        var dr = Math.PI / 180; // degree to radian
        
        var juliusDays = 2415020.75933 + 29.53058868*k + 0.0001178*time2 - 0.000000155*time3;
        juliusDays = juliusDays + 0.00033*Math.sin((166.56 + 132.87*time1 - 0.009173*time2)*dr); // Mean new moon
        
        var m = 359.2242 + 29.10535608*k - 0.0000333*time2 - 0.00000347*time3; // Sun's mean anomaly
        var mpr = 306.0253 + 385.81691806*k + 0.0107306*time2 + 0.00001236*time3; // Moon's mean anomaly
        var f = 21.2964 + 390.67050646*k - 0.0016528*time2 - 0.00000239*time3; // Moon's argument of latitude
        
        var c = (0.1734 - 0.000393*time1)*Math.sin(m*dr) + 0.0021*Math.sin(2*dr*m);
        c = c - 0.4068*Math.sin(mpr*dr) + 0.0161*Math.sin(dr*2*mpr);
        c = c - 0.0004*Math.sin(dr*3*mpr);
        c = c + 0.0104*Math.sin(dr*2*f) - 0.0051*Math.sin(dr*(m+mpr));
        c = c - 0.0074*Math.sin(dr*(m - mpr)) + 0.0004*Math.sin(dr*(2*f + m));
        c = c - 0.0004*Math.sin(dr*(2*f - m)) - 0.0006*Math.sin(dr*(2*f + mpr));
        c = c + 0.0010*Math.sin(dr*(2*f - mpr)) + 0.0005*Math.sin(dr*(2*mpr + m));
        
        var delta;
        if (time1 < -11) {
            delta = 0.001 + 0.000839 * time1 + 0.0002261 * time2 - 0.00000845 * time3 - 0.000000081 * time4;
        } else {
            delta = -0.000278 + 0.000265 * time1 + 0.000262 * time2;
        }
        
        var juliusDays0 = juliusDays + c - delta;
        
        return Math.floor(juliusDays0 + 0.5 + timeZone/24);
    }
    
    function calculateSunLongitude(juliusDays, timeZone) {
        juliusDays = (juliusDays - 0.5 - timeZone/24.0);
        
        var time1 = (juliusDays - 2451545.0 ) / 36525; // Time in Julian centuries from 2000-01-01 12:00:00 GMT
        var time2 = time1 * time1;
        var time3 = time2 * time1;
        var dr = Math.PI/180; // degree to radian
        var m = 357.52910 + 35999.05030*time1 - 0.0001559*time2 - 0.00000048*time3; // mean anomaly, degree
        var l0 = 280.46645 + 36000.76983*time1 + 0.0003032*time2; // mean longitude, degree
        
        var dl = (1.914600 - 0.004817*time1 - 0.000014*time2)*Math.sin(dr*m);
        dl = dl + (0.019993 - 0.000101*time1)*Math.sin(dr*2*m) + 0.000290*Math.sin(dr*3*m);
        
        var l = l0 + dl; // true longitude, degree
        l = l*dr;
        l = l - Math.PI*2*(Math.floor(l/(Math.PI*2))); // Normalize to (0, 2*PI)
        
        return Math.floor(l / Math.PI*6);
    }
    
    function calculateLunarMonth11(year, timeZone) {
        var offset = calculateJuliusDays(31, 12, year) - 2415021;
        var k = Math.floor(offset / 29.530588853);
        var newMoonDay = calculateNewMoonDay(k, timeZone);
        var sunLongitude = calculateSunLongitude(newMoonDay, timeZone); // sun longitude at local midnight
        if (sunLongitude >= 9) {
            newMoonDay = calculateNewMoonDay(k-1, timeZone);
        }
        return newMoonDay;
    }
    
    function calculateLeapMonthOffset(month11, timeZone) {
        var k = Math.floor((month11 - 2415021.076998695) / 29.530588853 + 0.5);
        var last = 0;
        var arc = calculateSunLongitude(calculateNewMoonDay(k + i, timeZone), timeZone);
        var i = 1; // We start with the month following lunar month 11
        do {
            last = arc;
            i++;
            arc = calculateSunLongitude(calculateNewMoonDay(k+i, timeZone), timeZone);
        } while (arc != last && i < 14);
        
        return i - 1;
    }
}